package no.ffi.gismoidm.invocations;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.PublicKey;
import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import no.ffi.gismoidm.attrex.BooleanOperand;
import no.ffi.gismoidm.attrex.TrueAlways;
import no.ffi.gismoidm.servicestubs.SubjectProperties;
import no.ffi.gismoidm.utils.IdentityStatement;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.EncryptedObject;
import no.ffi.tpm.crypto.SignedObject;

/**
 * This class holds the necessary elements for an authenticated
 * service invocation: Identitystatement, Parameters/Returns,
 * Nonce and Signature
 *
 * @author anders
 */
public class ServiceInvocationStateless implements ServiceInvocation {

    transient ServiceInvocationInternalObject so;
    IdentityStatement identityStatement,crossIdentityStatement;
    SignedObject blob;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Object o = blob.getObject();
        if (o instanceof ServiceInvocationInternalObject)
            so = (ServiceInvocationInternalObject)o;
        else
            so = null; // Still encrypted, decrypted during validate()
    }

    // Constructor for request objects (Class access)
    ServiceInvocationStateless(Serializable parameters, IdentityStatement clientAss, CryptoOperations crOp) {
        this(parameters,null,null,clientAss,null,null,crOp);
    }

    // Constructor for response objects
    ServiceInvocationStateless(Serializable returns, IdentityStatement clientAss, IdentityStatement serverAss, UUID nonce, CryptoOperations crOp) {
        this(null,returns,clientAss,serverAss,null,nonce,crOp);
    }


    // Constructor for cross identityStatement service responses
    ServiceInvocationStateless(Serializable returns, IdentityStatement clientAss, IdentityStatement serverAss, IdentityStatement crossAss, UUID nonce, CryptoOperations crOp) {
        this(null,returns,clientAss,serverAss,crossAss,nonce,crOp);
    }

    transient private SecretKey secretKey=null;
    public SecretKey getSecretKey() { return secretKey; }

    // General constructor - private
    private ServiceInvocationStateless(Serializable parameters, Serializable returns, IdentityStatement clientAss, IdentityStatement localAss, IdentityStatement crossAss, UUID nonce, CryptoOperations crOp) {
        so = new ServiceInvocationInternalObject();
        so.parameters = parameters;
        so.returns = returns;
        if (nonce == null)
            so.nonce = new UUID(System.currentTimeMillis(), (long)(Long.MAX_VALUE*Math.random())); // Timeliness info
        else
            so.nonce = nonce;
        identityStatement = localAss;
        crossIdentityStatement = crossAss;
        // Encrypt "so" with public key of the client
        if (clientAss == null) { // Request object, sign but do not encrypt
            try {
//                Signature s = Signature.getInstance(Config.SIGNATURE_ALGORITHM);
                blob = crOp.sign(so);
//                blob = new SignedObject(so, privKey, s);
            } catch (Exception e) { e.printStackTrace(); }
        } else { // Response object, encrypt and sign
            try {
                EncryptedObject encObj = crOp.encrypt(so, clientAss.getSubjectPublicKey());
                // Sign the encrypted object
                blob = crOp.sign(encObj);
                secretKey = encObj.secretKey;
            } catch (Exception e) { e.printStackTrace(); }
        }
    }

    // Verification of a stateless request means controlling the signer's signature.
    // There is no reply checking of nonce involved.
    public void validateRequest(PublicKey trustAnchor, long clockCounter) throws GismoIdmException {
            validateRequest(trustAnchor, clockCounter,false);
    }
    
    public void validateRequest(PublicKey trustAnchor, long clockCounter,boolean acceptsExpiredIS) throws GismoIdmException {
        // Check validity of the identity statement
        System.out.println("Validating identityStatement:");
        if (acceptsExpiredIS) 
            identityStatement.validateInferiorIdentityStatement(trustAnchor);
        else
            identityStatement.validateIdentityStatement(trustAnchor);
        System.out.println(identityStatement);
        PublicKey signerKey = identityStatement.getSubjectPublicKey();
        try {
            // Verify request signature
            System.out.println("Verifying signature");
            if (!CryptoOperations.verify(blob, signerKey))
                throw new ServiceInvocationInvalid("Response signature is wrong");
            
        } catch (java.security.NoSuchAlgorithmException e) { e.printStackTrace(); }
          catch (Exception e) { throw new GismoIdmException(e.getMessage()); }
    }

    // The response is encrypted and signed. Check signature first, then decrypt the content
    public void validateResponse(UUID requestNonce,IdentityStatement crossIdentityStatement, 
            String serviceName, BooleanOperand reqs, CryptoOperations crOp) throws GismoIdmException {
        validateResponse(requestNonce, crossIdentityStatement, serviceName, reqs, crOp, false);
    }
    public void validateResponse(UUID requestNonce,IdentityStatement crossIdentityStatement, 
            String serviceName, BooleanOperand reqs, CryptoOperations crOp, boolean acceptsExpiredIS) throws GismoIdmException {
        PublicKey trustAnchor = crOp.getTrustAnchor(); 
        try {
            if (acceptsExpiredIS) 
                identityStatement.validateInferiorIdentityStatement(trustAnchor);
            else 
                identityStatement.validateIdentityStatement(trustAnchor);
        } catch (GismoIdmException e) {
            // Primary identityStatement may not validate if it is a cross identityStatement, in
            // which case the crossIdentityStatement should build a path to the trust anchor
            if (crossIdentityStatement == null) throw new ServiceInvocationInvalid(e.getMessage());
            crossIdentityStatement.validateIdentityStatement(trustAnchor);
            identityStatement.validateIdentityStatement(crossIdentityStatement.getSubjectPublicKey());
//            if (!identityStatement.getIssuerPublicKey().equals(crossIdentityStatement.getSubjectPublicKey()))
//                throw new ServiceInvocationInvalid("Cross COI IS not issued to the local IS issuer");
        }
        if (!identityStatement.validateSubjectName(serviceName))
            throw new ServiceInvocationInvalid("Identity of service is not " + serviceName + " as expected, but " + 
                    identityStatement.getSubjectName());

        PublicKey signerKey = identityStatement.getSubjectPublicKey();
        try {
            // Verify response signature
            try {
                if (!crOp.verify(blob, signerKey))
                    throw new ServiceInvocationInvalid("Response signature is wrong");
            } catch (Exception e) { throw new ServiceInvocationInvalid(e.getMessage());}
            // Signature ok, now decrypt the private key
            Object o = blob.getObject();
            if (o instanceof EncryptedObject)
                try {
                   so = (ServiceInvocationInternalObject)
                       CryptoOperations.deserialize(crOp.decrypt((EncryptedObject)o));
                   secretKey = ((EncryptedObject)o).secretKey;
                } catch (Exception e) { throw new ServiceInvocationInvalid(e.getMessage());}
            // Verify that UUID is the same as in the request
            if (!requestNonce.equals(so.nonce))
                throw new ServiceInvocationInvalid("Response nonce value is not the same as in the request");
            // Verify that the client's requirements are met by the service's IS
            SubjectProperties prop = new SubjectProperties(identityStatement);
            //if (reqs == null) reqs = new TrueAlways();
            if (reqs!=null && !reqs.getBooleanValue(prop.getAttributes()).truthValue())
                throw new ServiceInvocationInvalid("Response IS does not meet client's access requriements");
        } catch (java.io.IOException e)  { e.printStackTrace(); }
    }

    public IdentityStatement getIdentityStatement() { return identityStatement; }
    public Serializable getParameters() { return so.parameters; }
    public Serializable getReturns() { return so.returns; }
    public int getAuthenticationProtocol() { return ServiceInvocationFactory.STATELESS;}
    public UUID getNonce() { return so.nonce; }

}
